package at.fhooe.mc.master.clc.server.businesslogic;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.logging.Level;
import java.util.logging.Logger;

import at.fhooe.mc.master.clc.server.dataaccess.DAOFactory;
import at.fhooe.mc.master.clc.server.dataaccess.ErrorUtil;
import at.fhooe.mc.master.clc.server.dataaccess.MetricsDAOFactory;
import at.fhooe.mc.master.clc.server.dataaccess.dao.IMetricsDao;
import at.fhooe.mc.master.clc.server.dataaccess.dao.ITestableDao;
import at.fhooe.mc.master.clc.server.domain.FrameworkTestClassMapping;
import at.fhooe.mc.master.clc.server.domain.ITestable;
import at.fhooe.mc.master.clc.server.domain.TestableWrapper;
import at.fhooe.mc.master.clc.shared.domain.FrameworkID;
import at.fhooe.mc.master.clc.shared.domain.metrics.IMetric;
import at.fhooe.mc.master.clc.shared.domain.metrics.Metric;
import at.fhooe.mc.master.clc.shared.domain.metrics.MetricKey;
import at.fhooe.mc.master.clc.shared.domain.metrics.MetricTimeValue;
import at.fhooe.mc.master.clc.shared.domain.metrics.MetricsValueType;

public class MetricsAdapter implements IMetricsAdapter
{
	/**
	 * Debug message switch; switch this to false before deploying.
	 */
	private static final boolean DEV_MODE = false;
	
	// not needed at the moment
//	private QuotaService qs = QuotaServiceFactory.getQuotaService();
	private IMetricsDao mDao = DAOFactory.getDAOFactory().getMetricsDao();
	
	private Map<FrameworkID, Map<MetricKey, List<ITestable>>> testClassCache;

	private static final Logger log = Logger.getLogger(MetricsAdapter.class
			.getCanonicalName());

	private ITestable createITestable(
			Class<? extends ITestable> classToBeCreated, 
			boolean setDistinguishable,
			FrameworkID pfid,
			MetricKey key)
	{
		ITestable testClass = null;
		if (classToBeCreated == null)
		{
			log.log(Level.SEVERE,
					"Test Class for ["
							+ pfid
							+ ", "
							+ key
							+ "] was not defined in the test-class mapping!");
		}
		else if (ITestable.class
				.isAssignableFrom(classToBeCreated))
		{
					
			try
			{
				Constructor<?> constructor = classToBeCreated.getConstructor(boolean.class);
				testClass = (ITestable) constructor.newInstance(true);
				
				if (setDistinguishable)
				{
					testClass.setDistinguishable(UUID.randomUUID().toString());
				}
			} 
			catch (SecurityException e)
			{
				e.printStackTrace();
			} 
			catch (NoSuchMethodException e)
			{
				e.printStackTrace();
			}
			catch (IllegalArgumentException e)
			{
				e.printStackTrace();
			} 
			catch (InvocationTargetException e)
			{
				e.printStackTrace();
			}
			catch (InstantiationException e)
			{
				e.printStackTrace();
			} 
			catch (IllegalAccessException e)
			{
				e.printStackTrace();
			}
		} 
		else
		{
			log.log(Level.SEVERE,
					"Test Class for ["
							+ pfid
							+ ", "
							+ key
							+ "] does not implement ITestable!");
		}
		
		return testClass;
	}
	
	/**
	 * Initializes the test classes dynamically by metrics key and framework id.
	 * A class of a certain type is only instantiated once here - and then reused as 
	 * a reference. This is important as some operations depend on the same class 
	 * (in objectify for example the class gets an id upon synchronous save, which is later
	 * needed when the entity is deleted (entity with null id cannot be deleted as it cannot 
	 * be identified)).
	 * 
	 * @see FrameworkTestClassMapping
	 */
	private void initTestDataCache()
	{
		testClassCache = new LinkedHashMap<FrameworkID, Map<MetricKey, List<ITestable>>>();

		// through this class we can speedup the process of dynamically creating
		// the test classes
		// using reflection. Already created classes will be stored and the
		// reference will then
		// be used instead of a new instance.
		Map<Integer, Map<Class<?>, List<ITestable>>> initCache = new HashMap<Integer, Map<Class<?>, List<ITestable>>>();
		
		for (FrameworkID pfid : FrameworkID.values())
		{
			if (pfid.isActive())
			{
				Map<MetricKey, List<ITestable>> classMap = new LinkedHashMap<MetricKey, List<ITestable>>();
				// create the test classes for each metrics key (according to
				// the framework id)
				for (MetricKey key : MetricKey.values())
				{
					if (key.isActive())
					{
						TestableWrapper wrapper = FrameworkTestClassMapping.getTestClassForFrameworkIdAndMetric(pfid, key);
						
						Class<? extends ITestable> classToBeCreated = wrapper.getTestClass();
						int amount = wrapper.getAmount();
						int groupKey = wrapper.getGroupKey();
						List<ITestable> testables = new ArrayList<ITestable>();

						if (initCache.containsKey(groupKey) &&
								initCache.get(groupKey) != null &&
								initCache.get(groupKey).get(classToBeCreated) != null)
						{
							testables = initCache.get(groupKey).get(classToBeCreated);
						}
						else
						{
							for (int i = 0; i < amount; i++)
							{
								// if amount is 1 we only create one and don't set it distinguishable
								ITestable testClass = null;
								testClass = createITestable(
										classToBeCreated, 
										amount == 1 ? false : true, 
										pfid, 
										key);
								
								if (testClass != null)
								{
									testables.add(testClass);
								}
							}
							
							if (initCache.get(groupKey) == null)
								initCache.put(groupKey, new LinkedHashMap<Class<?>, List<ITestable>>());
							
							initCache.get(groupKey).put(classToBeCreated, testables);
						}
						
						// assign the test-classes to the key
						classMap.put(key, testables);
						
					} // end if key active
				} // end for all metric keys

				// add the metric-key class mapping to the test class cache
				testClassCache.put(pfid, classMap);
			} // end if framework is active
		} // end for all persistence framework ids
	}

	private void runMetric(FrameworkID pfid, MetricKey key,
			Date timestamp, List<ITestable> testClasses)
	{
		if (DEV_MODE)
		{
			System.out.println("  ## Run: " + key + " for " + pfid);
		}
		
		try
		{
			// get the corresponding dao factory
			MetricsDAOFactory factory = MetricsDAOFactory.getDAOFactory(pfid);
			ITestableDao dao = factory.getTestableDao();
	
			// 1. Start the measurement metrics
			long start = 0;
			long stop = 0;
			int ok = 0;
			
			// 2. do some action with small chunk and big chunk using the DAOs ...
			switch (key)
				{
				case SMALL_CHUNK_SAVE:
				case BIG_CHUNK_SAVE:
					start = System.currentTimeMillis();
					ok = dao.save(testClasses.get(0));
					stop = System.currentTimeMillis();
					break;
				case SMALL_CHUNK_LOAD:
				case BIG_CHUNK_LOAD:
					start = System.currentTimeMillis();
					ITestable testable = dao.loadSpecific(testClasses.get(0).getClass(), 
							testClasses.get(0).getId());
					stop = System.currentTimeMillis();
					if (testable == null)
					{
						ok = ErrorUtil.DATASTORE_LOAD_ERROR;
						throw new MetricsException("[" + key +  "] failed to retrieve element with is [" + 
								testClasses.get(0).getId() + "] for [" + pfid + "].");
					}
						
					break;
				case SMALL_CHUNK_LOAD_ALL:
					start = System.currentTimeMillis();
					List<? extends ITestable> r = dao.loadAll(testClasses.get(0).getClass());
					stop = System.currentTimeMillis();
					if (r == null || r.isEmpty())
					{
						ok = ErrorUtil.DATASTORE_LOAD_ERROR;
						throw new MetricsException("[" + key +  "] failed to retrieve all elements for [" + pfid + "].");
					}
						
					break;
				case SMALL_CHUNK_UPDATE:
				case BIG_CHUNK_UPDATE:
					// Update currently not implemented...
					// TODO
					break;
				case SMALL_CHUNK_DELETE:
				case BIG_CHUNK_DELETE:
					start = System.currentTimeMillis();
					ok = dao.delete(testClasses.get(0), testClasses.get(0).getClass());
					stop = System.currentTimeMillis();
					break;
				case BIG_CHUNK_LOAD_FILTER_LIST:
				{
					start = System.currentTimeMillis();
					List<? extends ITestable> result = dao.loadFilteredEquals(testClasses.get(0).getClass(), 
							"stringList1", "string_entry7", 
							null, null, null, null, null, null);
					
					stop = System.currentTimeMillis();
					if (result == null || result.isEmpty())
					{
						ok = ErrorUtil.DATASTORE_LOAD_ERROR;
						throw new MetricsException("[" + key +  "] failed to retrieve filtered list.");
					}
					break;
				}
				case SMALL_CHUNK_SAVE_10:
					start = System.currentTimeMillis();
					ok = dao.save(testClasses);
					stop = System.currentTimeMillis();
					break;
				case SMALL_CHUNK_LOAD_FILTERED:
				{
					String filter = testClasses.get(4).getDistinguishable();
					String field = "stringField";
					
					boolean success = false;
					int tries = 10;
					int counter = 0;
					
					List<? extends ITestable> result = null;
					start = System.currentTimeMillis();
					
					// seems it takes some time sometimes to store the data, so we add some tries
					while (!success && counter < tries)
					{
						result = dao.loadFilteredEquals(
								testClasses.get(0).getClass(), 
								field, 
								filter, 
								null, null, null, null, null, null);
						
						success = result != null && !result.isEmpty();
							
						
						counter++;
					}
					stop = System.currentTimeMillis();
					
					if (result == null || result.isEmpty())
					{
						ok = ErrorUtil.DATASTORE_LOAD_ERROR;
						throw new MetricsException("[" + key +  "] failed to retrieve filter element for [" + pfid + "].");
					}
					break;
				}
				case SMALL_CHUNK_DELETE_ALL:
					start = System.currentTimeMillis();
					ok = dao.deleteAll(testClasses);
					stop = System.currentTimeMillis();
					break;
					
				default:
					break;
				}
			
			// ######################
			// TODO: add cpu mega cycle metrics later...
			//long start = qs.getCpuTimeInMegaCycles();
			// -> just add more metrics entries and then handle the measurement different here
			// 		>> start and stop will then be taken through the QuotaService...
			
			// TODO: Also add some interesting metrics for indexed vs unindexed... 
			
			// TODO: add join-operation metrics (that will be interesting for metrics!)
			
			// TODO: add metrics for filtering (might have different metrics according to frameworks)
			// ######################
			
			// save metrics only if the test ran ok (ok is only checked for save, 
			// update, delete)
			if (ok == 0)
			{
				// save metrics entry to datastore with the timestamp
				// for the complete cycle
				IMetric metric = new Metric(
						timestamp, 
						pfid, 
						key, 
						new MetricTimeValue(
								MetricsValueType.DOUBLE_VAL, 
								stop - start));
				
				mDao.saveMetric(metric);
			}
			else
			{
				if (DEV_MODE)
					System.out.println("## ERROR OCCURED IN DAO, CHECK LOG!");
				
				log.log(Level.SEVERE, ErrorUtil.getErrorMessage(ok));
			}
		}
		catch (Exception e)
		{
			log.log(Level.SEVERE, "Exception while running metric [" + 
					key.getLabel() +"] for: [" + 
					pfid.getLabel() + "]; Exception Message = " + 
					e.getMessage());
			
			if (DEV_MODE)
			{
				System.out.println(e.getMessage());
				e.printStackTrace();
			}
		}
	}

	@Override
	public void runCompleteMetricsCycle()
	{
		
		// IMPORTANT Note: JDO has problems with saving the same object after the 
		// object was deleted already - thus the objects are instantiated new for every cycle 
		// at the moment TODO: Investigate more and maybe file a bug-report.
//		if (testClassCache == null)
			initTestDataCache();

		// the time stamp to be used to safe the metrics
		Date cycleTimeStamp = new Date();

		// loop over all the frameworks to be tested
		for (FrameworkID fid : FrameworkID.values())
		{
			if (fid.isActive())
			{
				if (DEV_MODE) {System.out.println("## Start Metrics Cycle for: " + fid);}
				// create test data to be used for metrics
				if (testClassCache.get(fid) != null)
				{
					for (MetricKey key : MetricKey.values())
					{
						if (key.isActive())
						{
							if (testClassCache.get(fid).get(key) != null)
							{
								runMetric(fid, key, cycleTimeStamp,
										testClassCache.get(fid).get(key));
							} else
							{
								log.log(Level.SEVERE,
										"Test class(es) for Framework [" + fid
												+ "] and (active) key [" + key
												+ "] missing!");
							}
						}
					}
				} else
				{
					log.log(Level.SEVERE, "Test classes for Framework [" + fid
							+ "] were not initialized correctly!");
				}
				if (DEV_MODE) {System.out.println("## End Metrics Cycle for: " + fid);}
			}
			else
			{
				if (DEV_MODE) {System.out.println("## Skipped: " + fid);}
			}
		}
	}

}
